const Type = require('./type');

const requestAnimationFrame = function (callback) {
    setTimeout(callback, 1000 / 60);
};

function round(v, decimals) {
    return parseFloat(parseFloat(v).toFixed(decimals));
}

function cubic_bezier(t, x1, y1, x2, y2) {
    let  p0 = [0,0], p3=[1,1], p1=[x1, y1], p2=[x2, y2];
    
    let x = p0[0] * Math.pow(1-t, 3)
        + 3*p1[0]*t*Math.pow(1-t, 2)
        + 3*p2[0]*Math.pow(t, 2)*(1-t)
        + p3[0]*Math.pow(t, 3)
    ;
    
    let y = p0[1] * Math.pow(1-t, 3)
        + 3*p1[1]*t*Math.pow(1-t, 2)
        + 3*p2[1]*Math.pow(t, 2)*(1-t)
        + p3[1]*Math.pow(t, 3)
    ;
    
    return {
        x:round(x, 6),
        y:round(y, 6)
    };
}

function cubic_bezier_value(x, x1, y1, x2, y2) {
    let bt = 0;
    let et = 1;
    
    x = round(x, 6);
    let p={};
    
    do {
        let t = (bt+et)/2;
        p = cubic_bezier(t, x1, y1, x2, y2);
        
        if(p.x > x) {
            et = t;
        } else {
            bt = t;
        }
    } while(x != p.x);
    
    return p.y;
}


const ERASING = {
    liner: '0,0,1,1',
    ease: '.25,.1,.25,1',
    'ease-in': '.42,0,1,1',
    'ease-out': '0,0,.58,1',
    'ease-in-out': '.42,0,.58,1',
};

function convert_to_points(str) {
    return str.replace(/,/g, ' ').split(' ').filter(a => a.length>0).map(a => parseFloat(a));
}

function get_bezier_params(erasing, defaultParams) {
    let bezierParams = defaultParams ? defaultParams : convert_to_points(ERASING.liner);
    
    if(ERASING[erasing]) {
        bezierParams = convert_to_points(ERASING[erasing]);
    } else if(erasing) {
        bezierParams = convert_to_points(erasing);
    }
    
    return bezierParams;
}

const animate = function(options) {
    let refParams = options.refParams;
    let params = {};
    
    if(refParams) {
        params = options.params;
    } else {
        params = Type.clone(options.params);
    }
    
    let target = Type.clone(options.target);
    let erasing = options.erasing ? options.erasing : 'liner';
    let duration = options.duration ? options.duration : 300;
    let complete = options.duration ? options.complete : null;
    let update = options.update ? options.update : null;
    let begin = options.begin ? options.begin : null;
    let bezierParams = get_bezier_params(erasing);
    
    if(begin) {
        begin();
    }
    
    let offset = {};
    
    let control = {
        stopped:false,
        
        stop() {
            this.stopped = true;
            return this;
        }
    };
    
    for(let i in params) {
        if(Type.isUndefined(target[i])) {
            delete params[i];
        }
        
        if(target[i].value) {
            offset[i] = target[i].value - params[i];
        } else {
            offset[i] = target[i] - params[i];
        }
    }
    
    if(begin) {
        begin();
    }
    
    let anim = {};
    
    let startTime = new Date().getTime();
    let finishCount = 0;
    let totalCount = 0;
    
    for(let i in target) {
        anim[i] = {
            duration: target[i].duration ? target[i].duration : duration,
            bezierParams: target[i].erasing ? get_bezier_params(target[i].erasing) : bezierParams,
            startTime,
            oldval: params[i],
            offset: offset[i],
            finish:false
        };
    
        totalCount++;
    }
    
    let render = function() {
        if(control.stopped) {
            return;
        }
        
        let currentTime = new Date().getTime();

        for (let i in anim) {
            if(anim[i].finish) {
                continue;
            }
            
            let percent = (currentTime - anim[i].startTime)/anim[i].duration;
            percent = percent > 1 ? 1 : percent;
    
            params[i] = cubic_bezier_value(percent, ...anim[i].bezierParams) * anim[i].offset + anim[i].oldval;
            
            if(percent == 1) {
                finishCount ++;
                anim[i].finish = true;
                
                if(totalCount == finishCount) {
                    break;
                }
            }
        }
    
        update && update(params);
        
        if(totalCount == finishCount) {
            complete && complete(params);
            return;
        }
        
        requestAnimationFrame(render);
    };
    
    requestAnimationFrame(render);
    return control;
};

module.exports = animate;
